Skip to content

feat(mcp): report truthful index freshness from checkout evidence - #1561

Merged
DeusData merged 7 commits into
DeusData:mainfrom
tmonestudio:codex/bt-240-truthful-index-freshness
Nov 3, 2026
Merged

DeusData merged 7 commits into
DeusData:mainfrom
tmonestudio:codex/bt-240-truthful-index-freshness

Conversation

@tmonestudio

Copy link
Copy Markdown
Contributor

What does this PR do?

Makes verbose index_status distinguish the live checkout from the generation that was actually indexed.

  • persists indexed_checkout_sha only at the staged-generation publication boundary
  • reports bounded tracked/untracked checkout evidence
  • returns machine-readable current, stale, or fail-closed unknown
  • preserves the lean default response and legacy databases
  • handles Windows porcelain-v2 rename/copy records and bounded git subprocess failures

This is intentionally separate from #1181 and #1065.

Local verification

  • production Windows/MinGW build passed with the repository warnings-as-errors flags
  • focused test runner: git_context mcp store_nodes exited 0
  • existing database smoke: 27,369 nodes / 78,605 edges; legacy generation returned unknown/indexed_checkout_unavailable
  • database SHA-256 was identical before and after the smoke

Checklist

  • Every commit is signed off (git commit -s)
  • Full test suite is delegated to CI
  • Focused tests pass locally
  • New behavior is covered by reproduce-first tests

No runtime binary, cache, ACL, or corpus was modified.

@tmonestudio
tmonestudio requested a review from DeusData as a code owner August 12, 2026 04:21
@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

@DeusData

Copy link
Copy Markdown
Owner

Thank you @tmonestudio. The underlying issue is confirmed on current main: verbose index_status reports live checkout context, while the indexed generation stores no corresponding checkout identity. Persisting identity at the publication boundary is the right contract to review.

This is a broad correctness change despite the focused product claim: 11 files and about 1,300 added lines across git subprocess handling, MCP output, pipeline publication, store migration, and tests. It therefore needs a full storage/schema and fail-closed review rather than a quick UI pass. The current head is mergeable, but lint / lint and consequently ci-ok are red and the main test matrix was skipped. Please clear the lint failure before review.

Thank you for the reproduce-first coverage and legacy-database checks. The queue is full, so detailed feedback may take some time, but this is labeled and queued.

@DeusData

Copy link
Copy Markdown
Owner

Checking in — this has been quiet since 12 August and it is still open and still wanted, so here is where it stands with the friction removed.

The lint blocker is six clang-format violations in two files:

src/store/store.c:3171:94
src/store/store.c:3172:34
src/mcp/mcp.c:4504:80
src/mcp/mcp.c:4505:57
src/mcp/mcp.c:4505:78
src/mcp/mcp.c:4506:60

make -f Makefile.cbm lint-format reproduces it, and running clang-format over just those two files fixes it. One caveat that has bitten people here: it must be the Homebrew LLVM build — a standalone clang-format-20 produces spurious whole-file drift.

And an honest note on the rest, so the delay does not read as all yours. Even with lint green, this is not a fast merge: 11 files and ~1,300 lines spanning git subprocess handling, MCP output, pipeline publication, store migration and tests. It needs a full storage/schema and fail-closed review, and that review is ours to do and has not happened yet. Clearing lint is what lets the main test matrix actually run — it was skipped entirely on the last CI pass, so nobody has seen this change tested.

The underlying problem you identified is confirmed on current main and the contract you proposed — persisting checkout identity at the publication boundary — is still the right shape. Your reproduce-first coverage and the legacy-database checks were noted at the time and still stand.

If you have moved on, say so and we will take it from here rather than leaving it to age. If not, clear the lint and the matrix will finally have something to say.

@DeusData

DeusData commented Sep 1, 2026

Copy link
Copy Markdown
Owner

An update, and a decision that went in your favour.

#1727 independently added a top-level freshness to index_status as well — a string (fresh / stale / unknown) rather than your object, derived from clean Git snapshots taken before and after indexing. Neither of you could have seen the other: the PRs are independent, and git would have merged them without a conflict, leaving index_status with one key emitted twice at different types depending on merge order.

Your object stands. The reasoning is that a caller which learns why the graph is not current, and what to do about it, is better served than one that gets a single word — your reason, reasons[] and recommended_action are the difference. This PR is also standalone and already reviewed, so it does not wait on a six-PR stack. #1727 has been asked to drop its field and keep its last_index_attempt record, which is genuinely additive to yours.

One idea from that PR worth folding into yours if you touch the docs: "a dirty tree is never treated as evidence in either direction". Your implementation already behaves that way — a dirty tree cannot reach current because tracked changes force stale, and status_unavailable prevents a confident answer — but that sentence states the principle more clearly than anything currently written down here.

Where this stands otherwise: the storage/schema and fail-closed review you were owed is done and it passes — the three-DB-state migration (fresh, legacy-writable, legacy read-only) and the verdict ladder that never reaches current without indexed SHA, live HEAD, status availability and zero tracked/untracked. The only thing between this and merge is a clearance marker on the three cbm_popen calls in your test files, which is a maintainer step, not something you need to change.

Thanks for your patience — this has been open a long time, and it is the design that is being kept.

@DeusData

DeusData commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Clearance is done and I've now verified this end to end on today's main rather than on the August base: trial-merged onto fe85a6b2, built with ASan/UBSan, and ran git_context (10/10, seven of them yours), store_nodes 71/0, index_resilience 6/0, daemon_frontend 13/0, mcp 318/0, lint clean; then with the real binary in an isolated cache: index → current with the SHA matching HEAD, tracked edit → stale / tracked_changes_present, commit → stale / indexed_checkout_mismatch, untracked-only → unknown, re-index → current with the new SHA. The revert-check binds exactly where it should (never reading the persisted SHA reddens the store round-trip and four MCP verdict tests; dropping the tracked≠stale rule reddens the tracked tests and rename_counts_once). The fit is right too: one top-level freshness object, nothing else on the surface beyond the nullable indexed_checkout_sha column with its idempotent migration.

Two things are needed before it merges, both mechanical, and both caused by main moving under you:

  1. One conflict, src/mcp/mcp.c:44-53 (the enum block): your branch adds MCP_STATUS_SAMPLE_MAX / MCP_REASONS_MAX, main added MCP_QUERY_MAX_VISIBLE_ROWS / MCP_OUTPUT_BYTES_PER_TOKEN_ESTIMATE in the same spot. Keep both.
  2. Nine tests need "format":"json" in their index_status call: feat: make CLI and MCP output lean by default #1597 (merged this week) made the tree format the default for tool output, so JSON-substring assertions no longer match the default response. The calls are at tests/test_mcp.c:5711, 5736, 5843, 5881, 5937, 5988, 6046, 6076, 6131; main's own tool_index_status_includes_git_metadata shows the pattern. With that, mcp is 318/0. Line 5736 then runs past 100 columns, so run clang-format (the Homebrew LLVM one) over the file.

Non-blocking, take or leave: the verbose description at src/mcp/mcp.c:680-683 still says only "add worktree/shadow Git paths" — worth a clause about freshness; and recommended_action maps every unknown to reindex_to_record_indexed_checkout, which misleads for untracked_not_indexed / status_unavailable where the identity is recorded — a follow-up is fine.

Keep the sign-offs on the rebased commits and this merges on green. Thanks for the fast turnaround on the lint fix in August and for sticking with it.

@tmonestudio
tmonestudio force-pushed the codex/bt-240-truthful-index-freshness branch from ca7442b to b939a2d Compare September 7, 2026 20:26
@tmonestudio

Copy link
Copy Markdown
Contributor Author

Thanks for the CI report. I traced both failures to the same underlying issue: ci-ok was only aggregating the failed Unix shard, whose sole failing suite was complexity.

The failure was scheduler-sensitive, not a freshness regression: the shared-package scaling test observed nodes 244 -> 842 (ratio 3.45) while edges stayed 659 -> 1319 (ratio 2.00), exceeding the 2.75 node-ratio guard under the parallel wave. Re-running the suite locally produced the expected nodes 244 -> 484 (ratio 1.98) consistently.

Fix pushed in commit 6875483f: classify complexity as a serial/quiet-tail suite in scripts/run-tests-parallel.sh, alongside extraction, because both measure replicated-corpus scaling and are sensitive to concurrent allocator/CPU contention. No product logic or freshness behavior was changed.

Local verification on the updated source:

  • git_context: 7 passed, 3 platform skips
  • store_nodes: 71 passed
  • index_resilience: 6 passed, 1 platform skip
  • mcp: 315 passed, 11 platform skips
  • complexity: 5 passed repeatedly

Please rerun the Unix shard/CI checks for 6875483f.

@DeusData

DeusData commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Thank you for tracking down the failing shard and recording both the parallel and isolated counts. That evidence is useful.

Please separate the serial-suite workaround in 6875483 from this freshness PR. The reported change from 484 to 842 nodes is a difference in graph contents, not just elapsed time; moving the suite to the quiet tail may avoid the symptom, but does not yet establish whether the cause is shared test state, extraction nondeterminism, or another defect. Please preserve the failing run and fixture details so we can investigate that discrepancy separately rather than treating a serial green run as a correctness fix.

This does not withdraw our agreement on the freshness design. We also owe you the final integration review after our earlier merge commitment. Thank you for the repeated updates and your patience.

@tmonestudio
tmonestudio force-pushed the codex/bt-240-truthful-index-freshness branch from a7941a4 to e0b9642 Compare September 13, 2026 00:52
Signed-off-by: tmonestudio <tmonestudio@users.noreply.github.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you for splitting the complexity-suite workaround out as we asked. The branch is now the freshness change alone, which is the version we want to merge.

main has moved on since, and the branch now conflicts in two files: src/pipeline/pipeline.c and src/pipeline/pipeline_internal.h. Most of the churn comes from today's markdown-link pass (#1832) and the memory-core work. Could you rebase onto the current main? Everything else merges cleanly.

One heads-up so it doesn't surprise you: a rebase produces a new head, so we re-run our security clearance and verification on it before merging. That is routine and nothing you need to do. The design agreement from 2026-09-01 (your freshness object is the one that ships) still stands. Thank you for your patience with how long this one has taken.

DeusData added a commit that referenced this pull request Sep 30, 2026
#2167)

The background watcher only watches the project an open MCP session is
rooted in, and drops the watch when the last session for that project
closes. A repository indexed with `cli index_repository` (or from a
session rooted elsewhere) is never watched, but README promised
"Auto-sync keeps it fresh after that" for every index_repository target
and the MCP instructions said "Indexes auto-refresh". Nothing in the
status output said which projects were actually watched, so a stale
CLI-indexed repo looked like a watcher bug.

What gets watched is unchanged. Instead:

- index_status gains a `watch` object: `watched`, plus `strategy`,
  `poll_interval_ms` and `last_scan_at` when watched, or a `reason`
  (not_session_project, cli_session, auto_watch_off, watcher_disabled,
  not_registered, no_watcher) and a re-index hint when not. It does not
  touch the #1561 `freshness` key.
- The watcher publishes per-project strategy, cadence and last completed
  scan time through atomics and exposes them via
  cbm_watcher_project_info(); the daemon answers index_status through a
  session-scoped provider.
- README, docs/CONFIGURATION.md, docs/index.html, the index_status tool
  description and the MCP instructions now state the real scope.

`watcher_enabled` already shows in `config list` since v0.11.0 (680bc72).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
TFSebben pushed a commit to TFSebben/codebase-memory-mcp that referenced this pull request Sep 30, 2026
…a#2144)

index_repository blocks until the whole index completes. MCP clients with
a per-call deadline (Copilot for IntelliJ) give up on a large repository;
their cancel drops the daemon job's last subscriber, the daemon cancels
the worker, and every retry starts over, so the index never finishes.

Add an opt-in async mode and a status query to the same tool; the default
synchronous behaviour is unchanged:

- async: true starts the project's index job in the daemon, or joins the
  one already running for it, and returns at once with its state. The
  application holds one subscriber reference for an async job until its
  terminal publish, so the request returning, the client cancelling or
  the starting session closing no longer cancels it. Only daemon shutdown
  (or the final session of a non-permanent generation) ends it. Async
  requests never queue behind the physical job limit; a full daemon
  answers busy. async/status are stripped from the worker args, so an
  async request coalesces with an identical synchronous one.
- status: true reports the running or last job for the project
  (queued/running/cancelling/succeeded/failed/cancelled, started_at,
  finished_at, error summary) from a small per-project record kept in the
  daemon's job registry after the job itself is reaped. An unknown
  project is an error; a project indexed before this daemon generation
  reports idle. No freshness key is added: index_status keeps the DeusData#1561
  freshness object.
- async + status together, non-boolean values, and async with
  cross-repo-intelligence are refused. In-process servers (index worker,
  embedders) refuse both modes: nothing there outlives the call.
- On a temporary (non-permanent) daemon, async is refused when the
  request arrives on the one-shot `cli` tool channel and that session is
  the daemon's only live session: the daemon would stop and cancel the
  job the moment the command exits. The error points at `daemon start`
  (or an open MCP session). A long-lived MCP session stays a valid host
  even when it is the only one, which is the IDE case this exists for;
  status stays allowed everywhere. The daemon lifecycle is unchanged.
- New allocations go through the memory core (cbm_alloc/cbm_calloc/
  cbm_mem_strdup/cbm_free); blocks returned by non-core APIs are released
  with safe_free, so no file's raw allocator count grows.
- A cut-short synchronous call now offers the async alternative: in the
  cancellation text the daemon or frontend delivers (including the
  -32800 reply to a cancelled index_repository), and, because a timed-out
  client never reads that reply, as a notice on the next index_repository
  or status call for the project.
- The tool description and schema, the CLI help and the README explain
  the flags, the deadline problem and the polling pattern.

No server-side timeout or budget is introduced.

Tests (deterministic, held fake worker, bounded observation waits):
async returns before completion and status observes running then
succeeded; an async job survives its session closing while a sync job in
the same shape is still cancelled; validation errors; the cancellation
reply and the next-call notice carry the advice; a lone one-shot client
of a temporary daemon is refused async while status and an MCP session
on the same daemon are allowed; in-process refusal; schema; notice
helper.

Refs DeusData#2031

Fixes DeusData#2144

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
DeusData added a commit that referenced this pull request Sep 30, 2026
A project indexed from a directory that is not a git repository was never
refreshed after its first index: init_baseline classified it is_git=false
and poll_project/check_changes returned early for it forever, so the graph
silently went stale. The comment in init_baseline even claimed such folders
were "polled as a plain directory", which was never true.

Add an opt-in config key, watch_non_git (default false, read once at daemon
startup like watcher_enabled). When on, a non-git root is polled on the
existing adaptive cadence (cbm_watcher_poll_interval_ms) by a tree
signature: the indexer's own cbm_discover walk (same skip lists, .gitignore
stack and .cbmignore), sorted by path and folded over (relative path, size,
mtime). A changed signature takes the existing reindex path with the same
commit-after-success discipline as the #937 dirty signature. The baseline
leaves the signature "unknown", so the first poll reindexes once
(at-least-once, covering edits made while the daemon was down).

Runaway guard: only files discovery would index enter the signature, so
cbm's own .codebase-memory output (built-in skip) and the cache directory
(pruned by absolute path) can never make a finished reindex look like a
new change; a scratch folder under an unrelated dirty repository is polled
on its own files and never inherits the ancestor's dirty state.

cbm_file_info_t gains mtime_ns, filled from the stat discovery already
performs, so the signature needs no second stat per file and stays
UTF-8-correct on Windows (discovery's wide stat).

Docs: README and docs/CONFIGURATION.md now say non-git roots are not
watched by default and document watch_non_git. The staleness signal in
index_status is left to #1561's freshness object. Refs #1948.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
DeusData added a commit that referenced this pull request Sep 30, 2026
#2167)

The background watcher only watches the project an open MCP session is
rooted in, and drops the watch when the last session for that project
closes. A repository indexed with `cli index_repository` (or from a
session rooted elsewhere) is never watched, but README promised
"Auto-sync keeps it fresh after that" for every index_repository target
and the MCP instructions said "Indexes auto-refresh". Nothing in the
status output said which projects were actually watched, so a stale
CLI-indexed repo looked like a watcher bug.

What gets watched is unchanged. Instead:

- index_status gains a `watch` object: `watched`, plus `strategy`,
  `poll_interval_ms` and `last_scan_at` when watched, or a `reason`
  (not_session_project, cli_session, auto_watch_off, watcher_disabled,
  not_registered, no_watcher) and a re-index hint when not. It does not
  touch the #1561 `freshness` key.
- The watcher publishes per-project strategy, cadence and last completed
  scan time through atomics and exposes them via
  cbm_watcher_project_info(); the daemon answers index_status through a
  session-scoped provider.
- README, docs/CONFIGURATION.md, docs/index.html, the index_status tool
  description and the MCP instructions now state the real scope.

`watcher_enabled` already shows in `config list` since v0.11.0 (680bc72).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
DeusData added a commit that referenced this pull request Oct 4, 2026
#2167)

The background watcher only watches the project an open MCP session is
rooted in, and drops the watch when the last session for that project
closes. A repository indexed with `cli index_repository` (or from a
session rooted elsewhere) is never watched, but README promised
"Auto-sync keeps it fresh after that" for every index_repository target
and the MCP instructions said "Indexes auto-refresh". Nothing in the
status output said which projects were actually watched, so a stale
CLI-indexed repo looked like a watcher bug.

What gets watched is unchanged. Instead:

- index_status gains a `watch` object: `watched`, plus `strategy`,
  `poll_interval_ms` and `last_scan_at` when watched, or a `reason`
  (not_session_project, cli_session, auto_watch_off, watcher_disabled,
  not_registered, no_watcher) and a re-index hint when not. It does not
  touch the #1561 `freshness` key.
- The watcher publishes per-project strategy, cadence and last completed
  scan time through atomics and exposes them via
  cbm_watcher_project_info(); the daemon answers index_status through a
  session-scoped provider.
- README, docs/CONFIGURATION.md, docs/index.html, the index_status tool
  description and the MCP instructions now state the real scope.

`watcher_enabled` already shows in `config list` since v0.11.0 (680bc72).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Resolve five conflicts where main and this branch added code at the
same place; both sides are kept:
- src/mcp/mcp.c: main's index_status watch object (DeusData#2167) next to the
  freshness helpers
- src/pipeline/pipeline.c, pipeline_internal.h: main's resource-policy
  accessors next to cbm_pipeline_indexed_checkout_sha
- src/store/store.c: main's edges schema probe next to the
  indexed_checkout_sha column probe
- tests/test_git_context.c: main's inherited-git-env tests (DeusData#2003) next
  to the bounded worktree-status tests

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The freshness verdict reads cbm_git_worktree_status(), which spawned
git with plain cbm_popen. A GIT_DIR/GIT_WORK_TREE inherited from a git
hook or an editor overrides `git -C <root>`, so a clean checkout was
reported stale with the other repository's changes. Every other git
child already goes through cbm_popen_git since DeusData#2003; this probe now
does too.

worktree_status_ignores_inherited_repo_env pins it: a dirty decoy in
the hook environment must not leak into a clean fixture's status.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The status samples and the stored indexed_checkout_sha were the only
new raw malloc/realloc/free sites, which grew the memory-core ratchet
(scripts/lint-memory-core.py) for git_context.c and store.c. They now
use cbm_alloc/cbm_realloc/cbm_free and cbm_mem_strdup, with one class
per pointer (OTHER for the samples, STORE for the column value).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
.clang-format sets IndentCaseLabels: false; the status_code_ok switch
and a few wrapped calls in the new parser did not match it. Formatting
only; no behaviour change.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you, @tmonestudio -- and thank you for splitting the complexity workaround out when we asked; the branch you left is exactly the freshness change we agreed to ship on 1 September. Since main had moved a long way, we finished the last mile here rather than ask you for another rebase. Your commit is untouched; we added four commits on top:

  1. Merged current main (signed-off merge commit). The five conflicts were all places where main and your branch added code side by side -- main's new index_status "watch" object, the resource-policy accessors in the pipeline, an edges schema probe in the store, and new git-environment tests -- so both sides are kept. index_status now reports your "freshness" object next to "watch".
  2. The worktree status probe now spawns git through cbm_popen_git instead of cbm_popen. Since pre-commit hook fails the test suite: git exports GIT_DIR into hooks, which overrides git -C in tests/test_pipeline.c #2003 every other git child does this: an inherited GIT_DIR/GIT_WORK_TREE (from a git hook or an editor) overrides "git -C", and on a real checkout that made a clean tree read as stale with another repository's changes. A new test, worktree_status_ignores_inherited_repo_env, pins it.
  3. The status samples and the stored indexed_checkout_sha now allocate through the memory core (cbm_alloc / cbm_realloc / cbm_free / cbm_mem_strdup). Main added a ratchet that fails when a file gains raw malloc/free sites, and these were the only new ones.
  4. clang-format over the new status parser (the project style keeps case labels unindented). Formatting only.

We re-ran it end to end on a real repository: clean -> current, tracked edit -> stale, new commit -> stale with the checkout mismatch, untracked-only -> unknown, re-index -> current with the new SHA. The fail-closed ladder you designed did all the work there. Thank you for sticking with this one for so long.

@DeusData

Copy link
Copy Markdown
Owner

Thanks for your patience and for the freshness design. The October 20 update records that we took over the integration work, so there is no outstanding rebase request for you. Sorry for the delay on the remaining maintainer follow-up—I’ll follow up soon.

index_coverage_meta is one of the tables the graph digest pins column
by column, so the new indexed_checkout_sha column made the digest
reject every live store with a schema error: the live-schema
compatibility test failed, and test-impact, which digests the graph it
builds for HEAD, fell back to running every test for every change.

Record the checkout in a table of its own instead: index_checkout
(project primary key referencing projects ON DELETE CASCADE,
indexed_checkout_sha NOT NULL). A checkout is source identity, which
the graph digest deliberately does not bind, and the store already
keeps other per-project data (doc_link_*, observed_*) outside the
digested tables. index_coverage_meta keeps exactly the shape the digest
expects, so the digest needs no change and no version bump, and a new
table needs no migration of existing databases.

The row is written and cleared in the same coverage transaction, so a
failed replace keeps the previous identity, and it is removed with its
project. The store API is unchanged: cbm_coverage_meta_t carries
indexed_checkout_sha as before. Read-only opens of databases without
the table report no identity, as before.

store_graph_digest_indexed_checkout_identity_is_not_bound writes a
store through the real coverage API with and without a checkout SHA
and asserts that it still digests and that the SHA leaves the digest
unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Second maintainer refresh of DeusData#1561 (truthful index_status freshness),
onto main 9c19b5d, after the fix-up that keeps the indexed checkout in
its own index_checkout table (a0d0521). The first refresh is b44a22c.

Five files conflicted; each is resolved as main plus the branch change:

- src/pipeline/pipeline.c and src/pipeline/pipeline_incremental.c (two
  sites): main added .unresolved_calls_complete to the coverage-meta
  initializers where the branch added .indexed_checkout_sha. Both fields
  are kept, main first.

- src/store/store.h cbm_coverage_meta_t: main added
  unresolved_calls_complete where the branch added indexed_checkout_sha.
  Both fields are kept, main first.

- src/store/store.c init_schema: main added the unresolved_calls_complete
  column where the branch closed index_coverage_meta and created
  index_checkout. index_coverage_meta keeps main DDL exactly, and the
  index_checkout table follows it.

- src/store/store.c coverage replace: main moved the coverage-meta write
  out of cbm_store_coverage_replace_ex into coverage_write_metadata,
  whose caller owns the transaction and rolls it back on failure. The
  branch wrote or cleared the index_checkout row right after that
  write, so its two calls move into the helper: the upsert path returns
  index_checkout_write(s, project, meta->indexed_checkout_sha), the
  delete path returns index_checkout_write(s, project, NULL).
  index_checkout_write stays where the branch defined it, ahead of the
  new helpers.

- tests/test_store_nodes.c: main added
  store_coverage_shadow_excludes_unresolved_calls and its helper where
  the branch added store_coverage_meta_indexed_checkout_sha_roundtrip_and_rollback.
  Both tests are kept and registered, main first.

src/pipeline/pipeline_internal.h merged without a conflict: main raised
CBM_SEMANTIC_INDEX_VERSION to 13 for Next.js App Router routes, in a
separate hunk from the branch's cbm_pipeline_indexed_checkout_sha
declaration.

No semantic conflicts:

- The branch bumps no version. index_checkout is created by init_schema
  with IF NOT EXISTS and read only when present, so main's semantic
  index version 13 applies unchanged.

- The branch changes no existing function signature, and main changes
  none that the branch calls.

- Every main caller of cbm_store_coverage_meta_get added since the
  merge base, including the new Next.js upgrade test in test_pipeline.c,
  releases the result with cbm_store_coverage_meta_clear exactly once,
  which frees the checkout string. That test passes a shallow copy of
  the result to cbm_store_coverage_replace_ex and never frees the copy
  on its own.

No test call site needed adapting.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

DeusData commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Thank you, @tmonestudio, for this change and for your patience while main kept moving under it. Your freshness object and the fail-closed verdict ladder are exactly what we agreed to ship, and your tests (the store round-trip, the rollback on a failed replace, and the git-backed freshness cases) carry over unchanged.

One thing first, because it changes something we said in public: on 5 September we wrote that the nullable indexed_checkout_sha column on index_coverage_meta, with its idempotent migration, was the right fit. It was when we wrote it. Since then main gained a graph digest for change-aware test selection that fingerprints index_coverage_meta column by column, and an extra column there makes that digest refuse every current database, so test selection falls back to running everything. Rather than change the digest, we moved the checkout SHA out of that table. Your design stays the same; only the place the value is stored changes.

We pushed these commits on top of your branch:

  1. fix(store): keep the indexed checkout outside the digested tables. The checkout SHA now lives in a small table of its own, index_checkout (project, indexed_checkout_sha), written and cleared in the same transaction as the coverage metadata and removed with its project. Because it is a new table, existing databases need no migration, so the ALTER TABLE step is gone. The store API is unchanged: cbm_coverage_meta_t.indexed_checkout_sha reads and writes as before, and read-only opens of older databases still report no identity. A new test, store_graph_digest_indexed_checkout_identity_is_not_bound, writes a store with and without a checkout SHA and checks that it still digests and that the SHA leaves the digest unchanged.
  2. Merged current main (signed-off merge commit). Five files conflicted, all places where main and your branch added code side by side: main's new unresolved_calls_complete field sits next to your indexed_checkout_sha in the coverage metadata struct, in its initializers in pipeline.c and pipeline_incremental.c, and in the store schema, and main's new coverage test sits next to yours in test_store_nodes.c. Both sides are kept everywhere. One spot needed more than that: main moved the coverage metadata write into a helper, coverage_write_metadata, so the two index_checkout writes moved into that helper with it. The rest of current main merged cleanly, including the index format bump for the new Next.js route support; your change needs no version bump of its own, because index_checkout is created whenever a database is opened for writing.

Please pull before pushing again, since the branch has new commits from us. Thank you again for the careful work and for sticking with this one for so long.

@DeusData
DeusData merged commit 5689ea5 into DeusData:main Nov 3, 2026
26 checks passed
@DeusData

DeusData commented Nov 3, 2026

Copy link
Copy Markdown
Owner

Merged - thank you very much, @tmonestudio! index_status now reports freshness from real checkout evidence: it compares the indexed checkout with the current HEAD and the tracked changes, and fails closed with a clear verdict and action instead of guessing. Your verdict ladder and the git-backed tests are exactly what we wanted, and thank you again for your patience while main moved under this branch for so long.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker. ux/behavior Display bugs, docs, adoption UX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants